package com.zytech.zspider.engine;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.zytech.modules.lang.Ghost;
import com.zytech.modules.lang.Requires;
import com.zytech.modules.lang.Strings;
import com.zytech.modules.script.ScriptEvaluator;
import com.zytech.modules.script.ScriptResult;
import com.zytech.modules.script.groovy.GroovyScriptEvaluator;
import com.zytech.zspider.core.BadSpiderDefinitionException;
import com.zytech.zspider.core.SpiderDefinition;
import com.zytech.zspider.core.TaskDefinition;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * @author siuming
 */
public class DefaultSpiderEngine implements SpiderEngine {

    private final ScriptEvaluator scriptEvaluator;
    private SpiderProxyProvider spiderProxyProvider;

    public DefaultSpiderEngine() {
        this(GroovyScriptEvaluator.of(new String[]{"/META-INF/spider-engine.groovy"}));
    }

    /**
     * @param scriptEvaluator
     */
    public DefaultSpiderEngine(ScriptEvaluator scriptEvaluator) {
        Requires.notNull(scriptEvaluator, "scriptEvaluator must not be null.");
        this.scriptEvaluator = scriptEvaluator;
    }

    @Override
    public Iterator<TaskDefinition> assembleTasks(SpiderDefinition def) {
        Requires.notNull(def, "spiderDefinition must not be null.");
        Requires.notNull(def.getTaskDefinition(), new BadSpiderDefinitionException("spiderDefinition's taskDefinition must not be null."));

        if (!def.isEmptyBodyGenerator() &&
                (!def.isEmptyParamsGenerator() || !def.isEmptyParams())) {
            throw new BadSpiderDefinitionException("bodyGenerator and paramsGenerator or params are specified. there are mutually exclusive");
        }

        Map<String, String> headers = def.getHeaders();
        if (!def.isEmptyBodyGenerator()) {
            Set<String> bodys = assembleTaskBodys(def);
            return assembleTasksWithBodys(def, headers, bodys);
        }

        Set<Map<String, String>> params = assembleTaskParams(def);
        return assembleTasksWithParams(def, headers, params);
    }

    private Set<String> assembleTaskBodys(SpiderDefinition def) {
        ScriptResult scriptResult = getEvaluator().eval(def.getBodyGenerator());
        return castResultToStringSet(scriptResult);
    }

    private Set<String> castResultToStringSet(ScriptResult scriptResult) {
        Ghost<?> ghost = Ghost.me(scriptResult.getValue());
        if (ghost.openEyes().isArray()) {
            String[] strvals = SpiderEngineHelper.castScriptResultToStringArray(scriptResult);
            return ImmutableSet.copyOf(strvals);

        }
        if (ghost.openEyes().isIterable()) {
            Iterable<String> strvals = SpiderEngineHelper.castScriptResultToStringIterable(scriptResult);
            return ImmutableSet.copyOf(strvals);
        }
        return ImmutableSet.of(scriptResult.getValue().toString());
    }

    private Iterator<TaskDefinition> assembleTasksWithBodys(final SpiderDefinition def, final Map<String, String> headers, Set<String> bodys) {
        return Iterators.transform(bodys.iterator(), new Function<String, TaskDefinition>() {
            @Override
            public TaskDefinition apply(String body) {
                TaskDefinition task = def.getTaskDefinition().copy();
                assembleTaskProxy(def, task);
                return task.diffSettings(def.getSettings()).diffHeaders(headers).body(body);
            }
        });
    }

    private void assembleTaskProxy(SpiderDefinition def, TaskDefinition task) {
        String proxy = def.getSettings().get(TaskDefinition.Settings.HTTP_PROXY);
        if (null != getProxyProvider() && Strings.isBlank(proxy)) {
            SpiderProxy spiderProxy = getProxyProvider().provideProxy(def);
            task.setting(TaskDefinition.Settings.HTTP_PROXY, spiderProxy.toString());
        }
    }

    private Iterator<TaskDefinition> assembleTasksWithParams(final SpiderDefinition def, final Map<String, String> headers, Set<Map<String, String>> params) {
        return Iterators.transform(params.iterator(), new Function<Map<String, String>, TaskDefinition>() {

            @Override
            public TaskDefinition apply(Map<String, String> params) {
                // using def's taskDefinition as template
                TaskDefinition task = def.getTaskDefinition().copy();
                assembleTaskProxy(def, task);
                return task.diffSettings(def.getSettings()).diffParams(params).diffHeaders(headers);
            }
        });
    }

    private Set<Map<String, String>> assembleTaskParams(SpiderDefinition spiderDefinition) {
        return generateTaskParams(spiderDefinition.getParamsGenerator(), spiderDefinition.getParams());
    }

    private Set<Map<String, String>> generateTaskParams(Map<String, String> paramsGenerator, Map<String, String> merge) {
        if (paramsGenerator.isEmpty()) {
            return ImmutableSet.of(merge);
        }

        Map<String, Set<String>> candidates = Maps.transformValues(paramsGenerator,
                new Function<String, Set<String>>() {
                    @Override
                    public Set<String> apply(String input) {
                        return evalParamScript(input);
                    }
                }
        );

        return SpiderEngineHelper.cartesianProductParams(candidates, merge);
    }

    private Set<String> evalParamScript(String script) {
        ScriptResult scriptResult = getEvaluator().eval(script);
        return castResultToStringSet(scriptResult);
    }

    /**
     * @return
     */
    public ScriptEvaluator getEvaluator() {
        return scriptEvaluator;
    }

    /**
     * @return
     */
    public SpiderProxyProvider getProxyProvider() {
        return spiderProxyProvider;
    }

    /**
     * @param spiderProxyProvider
     */
    public void setProxyProvider(SpiderProxyProvider spiderProxyProvider) {
        Requires.notNull(spiderProxyProvider, "spiderProxyProvider must not be null.");
        this.spiderProxyProvider = spiderProxyProvider;
    }
}
